feat(tbor): implement AesGenerateKey + AesEncryptDecrypt (masked-key AES) - #584
Conversation
b3e53f6 to
427588b
Compare
0d4f1fc to
12e2dc4
Compare
427588b to
db29d52
Compare
12e2dc4 to
c89889c
Compare
db29d52 to
3655e3b
Compare
c89889c to
0f1655f
Compare
3655e3b to
2e032a4
Compare
0f1655f to
f9d5579
Compare
2e032a4 to
c4bd905
Compare
f9d5579 to
c082215
Compare
The base branch was changed.
Jayant Gandhi (jaygmsft)
left a comment
There was a problem hiding this comment.
approving with suggestions
c082215 to
2f3f0f2
Compare
There was a problem hiding this comment.
Pull request overview
Adds TBOR support for masked-key AES operations by introducing two new in-session opcodes: AesGenerateKey (0x15) to generate a random AES key and return it as an AEAD-masked blob, and AesEncryptDecrypt (0x16) to AES-CBC encrypt/decrypt a message using a caller-supplied masked AES key.
Changes:
- Wire in new AES opcodes through session classification, dispatcher routing, and session-control enforcement.
- Implement firmware handlers for AES key generation (masked blob output) and AES-CBC encrypt/decrypt (in-place unmask + zero-copy response fill).
- Add corresponding TBOR wire schemas, host-side wrappers, docs, and emulator integration tests.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| fw/core/lib/src/op.rs | Routes new AES opcodes as InSession for session-control enforcement. |
| fw/core/lib/src/ddi/tbor/mod.rs | Adds opcode constants, dispatch wiring, and session classifiers for 0x15/0x16. |
| fw/core/lib/src/ddi/tbor/aes_generate_key.rs | New FW handler to generate random AES key and return an AEAD-masked blob. |
| fw/core/lib/src/ddi/tbor/aes_encrypt_decrypt.rs | New FW handler to unmask AES key in-place and perform AES-CBC into reserved response slots. |
| fw/core/ddi/tbor/types/src/lib.rs | Exposes new AES wire-schema modules from the FW TBOR types crate. |
| fw/core/ddi/tbor/types/src/aes_generate_key.rs | Defines TBOR wire schema + constants for AesGenerateKey. |
| fw/core/ddi/tbor/types/src/aes_encrypt_decrypt.rs | Defines TBOR wire schema + constants for AesEncryptDecrypt. |
| docs/tbor-ddi/README.md | Documents new TBOR command table entries for opcodes 0x15/0x16. |
| docs/tbor-ddi/commands/aes_generate_key.md | Adds command documentation for AesGenerateKey. |
| docs/tbor-ddi/commands/aes_encrypt_decrypt.md | Adds command documentation for AesEncryptDecrypt. |
| ddi/tbor/types/tests/commands/mod.rs | Registers new AES command integration-test modules. |
| ddi/tbor/types/tests/commands/aes_generate_key.rs | Adds emu integration tests for AES key generation across sizes/scopes and reject paths. |
| ddi/tbor/types/tests/commands/aes_encrypt_decrypt.rs | Adds emu integration tests for CBC round-trips, chaining IV, tamper reject, and unwrap→AES interop. |
| ddi/tbor/types/src/lib.rs | Exposes new host-side TBOR wrapper modules. |
| ddi/tbor/types/src/aes_generate_key.rs | Adds host-side request/response wrapper for AesGenerateKey. |
| ddi/tbor/types/src/aes_encrypt_decrypt.rs | Adds host-side request/response wrapper for AesEncryptDecrypt. |
2f3f0f2 to
86337b4
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
fw/core/lib/src/ddi/tbor/aes_generate_key.rs:133
resolve_masking_keyis called after generating the random AES key. If the scope has no masking key (e.g.,SecurityDomainbeforeCreateSD) orSessionscope on Uno (wheresession_masking_keyreturnsUnsupportedCmd), the handler will still burn RNG and allocate key material before failing. Resolving the masking key first matches the doc comment about failing cheaply and avoids unnecessary keygen work.
// Generate the random AES key into scratch.
let key_buf = alloc.dma_alloc(key_len)?;
pal.aes_gen_key(io, key_buf).await?;
let masking_key = resolve_masking_key(pal, io, scope, sess_id)?;
docs/tbor-ddi/commands/aes_generate_key.md:35
- This doc implies
scope = Sessionalways works, but the firmware'sresolve_masking_keynotes that per-session masking keys are only provisioned on std/emu today; on Uno (hardware)scope = Sessionfails withUnsupportedCmd. Adding the same platform note asunwrap_key.mdwould prevent confusion for hardware-targeting callers.
- `Session` → the per-session masking key (works for any Active session,
including before `PartFinal`).
docs/tbor-ddi/commands/aes_encrypt_decrypt.md:19
- The command selects the masking key based on the masked key’s recorded scope, but on Uno (hardware)
scope = Sessionis not currently supported (persession_masking_key/resolve_masking_key), and will fail withUnsupportedCmd. Consider adding a brief platform note here (as done inunwrap_key.md) so callers don’t assume Session-scope masked keys work on hardware.
[`UnwrapKey`](./unwrap_key.md)). The device reads the masked key's scope
from its cleartext, tag-bound metadata to select the masking key, unmasks
the key on-device (verifying the AEAD tag), runs the AES-CBC transform
zero-copy — reading the request message and writing the transformed message
86337b4 to
b383791
Compare
…AES) Add the TBOR AES crypto command pair, stacked on the RSA-AES key-unwrap commands, following the MBOR AES commands but with the TBOR masked-key (stateless) model. AesGenerateKey (opcode 0x15): generate a fresh random AES key (128 / 192 / 256 bits) and return it masked (AEAD-GCM-256) under the requested scope's masking key. Non-bulk sizes only (mirroring MBOR); no vault `key_id` / `key_tag`. The masked-blob length is fixed by the key size, so the handler reserves the response slot up front and masks the generated key straight into it (encoder `*_reserve` + `decode_mut`) — no scratch buffer and no copy. AesEncryptDecrypt (opcode 0x16): AES-CBC encrypt or decrypt a host-supplied message with a caller-held masked AES key. Fully zero-copy: the request `masked_key` is `#[tbor(mutable)]`, so the handler `decode_mut`s the request and `unmask`s the blob in place, then the non-in-place `aes_cbc_enc_dec` reads the request message and writes the ciphertext / chaining IV straight into the reserved response slots — no blob / key scratch copy and no message copy. Rejects non-AES key kinds (`InvalidKeyType`) and direction-permission mismatches (`InvalidPermissions`); the recovered key is wiped on every path. AES key import is already provided by `UnwrapKey` (0x14, key class Aes); an emu test exercises the unwrap -> encrypt/decrypt round-trip. Reuses the shared session/masking helpers (`validate_active_session`, `resolve_masking_key`) and the encoder reserve/fill support from the key-unwrap PR below it in the stack. Wires both opcodes through the fw dispatcher, the `is_known_opcode` / `is_in_session` / `needs_session_id_cross_check` classifiers, and `op::SessionCtrl::from_tbor_opcode`. Adds fw + host wire schemas, command docs, and emu tests: keygen round-trip across all sizes/scopes, encrypt->decrypt round-trip, CBC chaining IV, tamper / bad-length rejects, and the UnwrapKey->AES cross-command round-trip. The AES type tests follow the api_rev.rs pilot pattern rather than a file-level `#![cfg(feature = "emu")]`: the module is gated on `any(emu, mock, sock)` and only the individual `_emu` tests (plus their FW-handler-only imports) carry `#[cfg(feature = "emu")]`, so the shared masked-key helpers/constants aren't limited to the emu backend. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0b09e50a-a9be-4bae-b347-d42dc775a258
b383791 to
ff12a1c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (3)
docs/tbor-ddi/commands/aes_encrypt_decrypt.md:73
- This command can also fail with
MaskedKeyDecodeFailedfor malformed masked-key metadata (bad magic/version/padding), because the handler callspeek_metadata/unmaskfrom the masked-key AEAD format. The errors table currently lists onlyAesGcmDecryptTagDoesNotMatch, which covers tag failures but not metadata decode failures.
| `SessionNotFound` | `session_id` does not refer to an allocated slot, or the slot is not `Active` |
| `InvalidArg` | Unknown `op`, IV not exactly 16 bytes, or `msg` empty / not a multiple of 16 / over 1024 bytes |
| `AesGcmDecryptTagDoesNotMatch` | The masked key's AEAD tag failed to verify (tampered or wrong-scope blob) |
| `InvalidKeyType` | The recovered key is not a non-bulk AES key |
| `InvalidPermissions` | The key lacks the permission for the direction (`encrypt` / `decrypt`) |
docs/tbor-ddi/commands/aes_generate_key.md:34
- The docs state
Sessionscope works for any Active session, but on Uno hardware the per-session masking key is not provisioned yet (seeresolve_masking_key's platform note). Consider adding the same platform note used inunwrap_key.mdso callers don’t assumescope = Sessionworks on hardware.
- `Session` → the per-session masking key (works for any Active session,
including before `PartFinal`).
docs/tbor-ddi/commands/aes_encrypt_decrypt.md:23
- The description implies
Session-scoped masked keys are generally usable, but on Uno hardware the per-session masking key is not provisioned yet (seeresolve_masking_keyplatform note). Adding a brief platform note here would prevent callers from assumingscope = Sessionworks on hardware.
This issue also appears on line 69 of the same file.
[`UnwrapKey`](./unwrap_key.md)). The device reads the masked key's scope
from its cleartext, tag-bound metadata to select the masking key, unmasks
the key on-device (verifying the AEAD tag), runs the AES-CBC transform
zero-copy — reading the request message and writing the transformed message
plus the updated chaining IV straight into the response buffer — so the
host can chain subsequent CBC blocks. This is the TBOR analogue of
MBOR `AesEncryptDecrypt`, keyed by a masked blob rather than a vault
`key_id`. Nothing is persisted and the recovered key is wiped.
Summary
Adds the TBOR AES crypto command pair, stacked on the RSA-AES
key-unwrap commands (#583), following the MBOR AES commands but with the
TBOR masked-key (stateless) model — no vault
key_id/key_tag.AesGenerateKey(opcode0x15)Generate a fresh random AES key (128 / 192 / 256 bits, non-bulk only,
mirroring MBOR) and return it masked (AEAD-GCM-256) under the
requested scope's masking key. The masked-blob length is fixed by the key
size, so the handler reserves the response slot up front and masks the
generated key straight into it (encoder
*_reserve+decode_mut) — noscratch buffer, no copy.
AesEncryptDecrypt(opcode0x16)AES-CBC encrypt or decrypt a host-supplied message with a caller-held
masked AES key. Fully zero-copy:
masked_keyis#[tbor(mutable)]→ the handlerdecode_mutsthe request and
unmasks the blob in place, using the recoveredkey directly (no blob / key scratch copy);
msg/ivslots are reserved, and the non-in-placeaes_cbc_enc_decreads the request message and writes the ciphertext /chaining IV straight into the response (no message copy).
Rejects non-AES key kinds (
InvalidKeyType) and direction-permissionmismatches (
InvalidPermissions); the recovered key is wiped on everypath (including the unmask-failure path).
AES key import
Already provided by
UnwrapKey(0x14, key classAes) in #583 — anemu test exercises the
UnwrapKey→AesEncryptDecryptround-trip.Wiring
Both opcodes are routed through the fw dispatcher, the
is_known_opcode/
is_in_session/needs_session_id_cross_checkclassifiers, andop::SessionCtrl::from_tbor_opcode(InSession, CO/CU). Reuses the sharedvalidate_active_session/resolve_masking_keyhelpers and the encoderreserve/fill support from #583.
Tests / validation
SD-scope + unknown-size rejects; encrypt→decrypt round-trip all sizes,
CBC chaining IV, tamper + bad-length rejects, and the
UnwrapKey→AEScross-command round-trip.
clippyclean, nightlyfmt+copyrightclean, Uno (thumbv7em) build passes.docs/tbor-ddi/commands/{aes_generate_key,aes_encrypt_decrypt}.md0x15,0x16).